Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • Refactor
    • Internal improvements to detector evaluation and handler architecture for improved maintainability and extensibility of the workflow engine system.

… stateful detector (#80168)

This adds a hook that can be implemented to produce an occurrence
specific to the detector that is subclassing the StatefulDetector.

Also change the signature of evaluate to return a dict keyed by groupkey
instead of a list. This helps avoid the chance of duplicate results for
the same group key.

<!-- Describe your PR here. -->
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR refactors the detector evaluation system to use stateful handlers and dict-based result mapping. The DetectorHandler API is updated to return keyed results by DetectorGroupKey instead of lists, and a new group_type property is added to the Detector model to resolve group types by slug.

Changes

Cohort / File(s) Summary
Detector Handler Base Class Migration
src/sentry/incidents/grouptype.py
Changed MetricAlertDetectorHandler to inherit from StatefulDetectorHandler[QuerySubscriptionUpdate] instead of DetectorHandler[QuerySubscriptionUpdate], with minimal evaluate method implementation.
Detector Model Enhancement
src/sentry/workflow_engine/models/detector.py
Added group_type property that resolves group type by slug using the GroupType registry; refactored detector_handler to use this new property instead of inline lookup.
Detector Processor API Refactoring
src/sentry/workflow_engine/processors/detector.py
Refactored evaluation pipeline: process_detectors now returns list of tuples with dict-keyed results; DetectorHandler.evaluate and StatefulDetectorHandler.evaluate return dict[DetectorGroupKey, DetectorEvaluationResult]; introduced build_occurrence_and_event_data hook for constructing occurrence and event data on non-OK transitions; switched result accumulation from list/append to dict-keying pattern.
Test Suite Refactoring
tests/sentry/workflow_engine/processors/test_detector.py
Updated all test assertions to expect dict-based results; introduced MockDetectorStateHandler as stateful test double; added helper utilities (create_detector_and_conditions, build_handler, assert_updates, build_mock_occurrence_and_event); added freeze_time decorator; reworked multi-group and dedupe scenarios to validate dict-keyed result structure and kafka occurrence production.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DetectorProcessor as Detector Processor
    participant Handler as StatefulDetectorHandler
    participant ResultDict as Result Dict<br/>(by GroupKey)
    
    Client->>DetectorProcessor: process_detectors()
    activate DetectorProcessor
    
    loop for each detector
        DetectorProcessor->>Handler: evaluate(data_packet)
        activate Handler
        
        loop for each group_key in data_packet
            Handler->>Handler: evaluate_group_key_value()
            alt new_status != OK
                Handler->>Handler: build_occurrence_and_event_data(group_key, value, status)
                Handler->>Handler: create DetectorEvaluationResult
            else status OK
                Handler->>Handler: create DetectorEvaluationResult (no event data)
            end
            Handler->>ResultDict: result[group_key] = evaluation_result
        end
        
        Handler-->>DetectorProcessor: dict[GroupKey → Result]
        deactivate Handler
        
        DetectorProcessor->>DetectorProcessor: collect results tuple
    end
    
    DetectorProcessor-->>Client: list[(detector, dict[GroupKey → Result])]
    deactivate DetectorProcessor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 From lists to dicts, our rabbits hop with glee,
Stateful handlers guide results wild and free,
Group keys map to evaluations clear,
The detector flow evolves, no fear!
GroupType resolves, events take flight,
Testing tunnels deep—hooray, all tight! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: workflow-engine-stateful-detector-after' is vague and generic, using a non-descriptive format that doesn't convey meaningful information about the actual changes in the changeset. Revise the title to clearly describe the main change, such as 'Refactor detector handler to use StatefulDetectorHandler with dict-based results' or similar, avoiding generic automated test naming patterns.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch workflow-engine-stateful-detector-after

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/sentry/workflow_engine/processors/detector.py (1)

226-244: ⚠️ Potential issue | 🟡 Minor

Stale docstring: still says "list" instead of "dict".

Line 230: the docstring says "returns a list of DetectorEvaluationResult" but the method now returns a dict[DetectorGroupKey, DetectorEvaluationResult].

📝 Proposed fix
     def evaluate(
         self, data_packet: DataPacket[T]
     ) -> dict[DetectorGroupKey, DetectorEvaluationResult]:
         """
-        Evaluates a given data packet and returns a list of `DetectorEvaluationResult`.
+        Evaluates a given data packet and returns a dict of `DetectorGroupKey` to `DetectorEvaluationResult`.
         There will be one result for each group key result in the packet, unless the
         evaluation is skipped due to various rules.
         """
tests/sentry/workflow_engine/processors/test_detector.py (1)

536-559: ⚠️ Potential issue | 🟡 Minor

The expected occurrence should be built with the same group_key and value as the evaluation parameters.

At lines 536-538, build_mock_occurrence_and_event is called with group_key="val1" and value=6, but the actual evaluation at lines 547-548 uses group_key="group_key" and value=10. Although IssueOccurrence.__eq__ only compares the id field (so the test passes), the expected result semantically doesn't match the parameters being tested. The test should use consistent parameters:

🐛 Proposed fix
-            occurrence, event_data = build_mock_occurrence_and_event(
-                handler, "val1", 6, PriorityLevel.HIGH
-            )
+            occurrence, event_data = build_mock_occurrence_and_event(
+                handler, "group_key", 10, PriorityLevel.HIGH
+            )
🤖 Fix all issues with AI agents
In `@src/sentry/incidents/grouptype.py`:
- Around line 10-12: MetricAlertDetectorHandler currently inherits
StatefulDetectorHandler but doesn't implement the four required abstract
methods, causing instantiation errors; either implement the methods
counter_names, get_dedupe_value, get_group_key_values, and
build_occurrence_and_event_data inside MetricAlertDetectorHandler with the
appropriate logic for MetricAlertFire, or explicitly prevent instantiation
(e.g., make the class abstract or raise in __init__) until implementations exist
so Detector.detector_handler(self) won't instantiate a broken handler.

In `@tests/sentry/workflow_engine/processors/test_detector.py`:
- Around line 190-192: The test passes an incorrect value for group_2 to
build_mock_occurrence_and_event: change the third argument from 6 to 10 so the
call to build_mock_occurrence_and_event(detector.detector_handler, "group_2",
10, PriorityLevel.HIGH) matches the data packet; update any other test
invocations of build_mock_occurrence_and_event that mismatch their corresponding
packet values to use the actual packet value to avoid silent test breakage if
the helper starts using the value.
🧹 Nitpick comments (3)
tests/sentry/workflow_engine/processors/test_detector.py (3)

27-34: Mutable class attribute should use ClassVar annotation.

Per Ruff RUF012, mutable class attributes like counter_names should be annotated with typing.ClassVar to signal they aren't instance attributes.

📝 Proposed fix
+from typing import ClassVar
+
 class MockDetectorStateHandler(StatefulDetectorHandler[dict]):
-    counter_names = ["test1", "test2"]
+    counter_names: ClassVar[list[str]] = ["test1", "test2"]

47-50: Consider using start()/stop() or addCleanup for the patcher.

Using __enter__/__exit__ directly on the patcher works but is unconventional. The idiomatic pattern is:

self.sm_comp_patcher = mock.patch.object(StatusChangeMessage, "__eq__", status_change_comparator)
self.sm_comp_patcher.start()
self.addCleanup(self.sm_comp_patcher.stop)

This also removes the need for a manual tearDown.

Also applies to: 82-84


244-275: Remove unused value parameter from build_mock_occurrence_and_event.

The value parameter is never used in the function body. All call sites pass value=6 regardless of actual group values, making the parameter misleading—for example, line 191 passes value=6 for "group_2" when the data packet has "group_2": 10. Remove the parameter from the function signature and all call sites.

Comment on lines 10 to +12
# TODO: This will be a stateful detector when we build that abstraction
class MetricAlertDetectorHandler(DetectorHandler[QuerySubscriptionUpdate]):
def evaluate(
self, data_packet: DataPacket[QuerySubscriptionUpdate]
) -> list[DetectorEvaluationResult]:
# TODO: Implement
return []
class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate]):
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find StatefulDetectorHandler and verify its abstract methods
rg -n "class StatefulDetectorHandler" --type=py -A 30

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 2872


🏁 Script executed:

# Search for references to metric_alert_fire
rg -n "metric_alert_fire" --type=py -C 3

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 580


🏁 Script executed:

# Search for MetricAlertDetectorHandler usage and instantiation
rg -n "MetricAlertDetectorHandler" --type=py -C 3

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 825


🏁 Script executed:

# Look at the full context of the grouptype.py file around those lines
cat -n src/sentry/incidents/grouptype.py | head -50

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 1347


🏁 Script executed:

# Check for build_occurrence_and_event_data abstract method
rg -n "build_occurrence_and_event_data" --type=py -C 2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 1547


🏁 Script executed:

# Look at the full StatefulDetectorHandler class definition
rg -n "class StatefulDetectorHandler" --type=py -A 100 | head -120

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 9597


🏁 Script executed:

# Search for how detector_handler property/attribute is used in Detector class
rg -n "detector_handler" --type=py -C 2

Repository: admin-coderabbit/coderabbit-sentry-mirror

Length of output: 5431


MetricAlertDetectorHandler does not implement required abstract methods from StatefulDetectorHandler.

StatefulDetectorHandler declares four abstract methods (counter_names, get_dedupe_value, get_group_key_values, build_occurrence_and_event_data). Since MetricAlertDetectorHandler contains only pass, attempting to instantiate it will raise TypeError at runtime. The detector_handler property in Detector instantiates handlers via group_type.detector_handler(self), so any detector using MetricAlertFire will fail.

Additionally, the TODO on line 10 ("This will be a stateful detector when we build that abstraction") is stale—the class already inherits StatefulDetectorHandler.

If this is intentionally WIP, implement the abstract methods or prevent instantiation. Otherwise, complete the implementation.

🤖 Prompt for AI Agents
In `@src/sentry/incidents/grouptype.py` around lines 10 - 12,
MetricAlertDetectorHandler currently inherits StatefulDetectorHandler but
doesn't implement the four required abstract methods, causing instantiation
errors; either implement the methods counter_names, get_dedupe_value,
get_group_key_values, and build_occurrence_and_event_data inside
MetricAlertDetectorHandler with the appropriate logic for MetricAlertFire, or
explicitly prevent instantiation (e.g., make the class abstract or raise in
__init__) until implementations exist so Detector.detector_handler(self) won't
instantiate a broken handler.

Comment on lines +190 to 192
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading value argument for group_2.

group_2 has value 10 in the data packet (Line 177), but 6 is passed to build_mock_occurrence_and_event on Line 191. This only "works" because value is unused in the helper. If value is ever used in the occurrence, this test will silently produce wrong expectations.

📝 Proposed fix
         occurrence_2, event_data_2 = build_mock_occurrence_and_event(
-            detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
+            detector.detector_handler, "group_2", 10, PriorityLevel.HIGH
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
)
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 10, PriorityLevel.HIGH
)
🤖 Prompt for AI Agents
In `@tests/sentry/workflow_engine/processors/test_detector.py` around lines 190 -
192, The test passes an incorrect value for group_2 to
build_mock_occurrence_and_event: change the third argument from 6 to 10 so the
call to build_mock_occurrence_and_event(detector.detector_handler, "group_2",
10, PriorityLevel.HIGH) matches the data packet; update any other test
invocations of build_mock_occurrence_and_event that mismatch their corresponding
packet values to use the actual packet value to avoid silent test breakage if
the helper starts using the value.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant